fix(service-analytics): resolve a user (and tree) dimension's display label through its reference, as lookup already does - #17470
Conversation
… through spec's arbiter Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
…er-dimension-display-name
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 46332726694e99851d1e794aed90ee80f4ab3cf1 && git checkout 46332726694e99851d1e794aed90ee80f4ab3cf1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c4d1759fa3c73b68cd16df8bf8562c4f86268dc6 44918e39daec2d24b6390cc52299303065a93088 && git checkout -B drift-repro c4d1759fa3c73b68cd16df8bf8562c4f86268dc6 && git merge --no-ff 44918e39daec2d24b6390cc52299303065a93088
node scripts/docs-audit/affected-docs.mjs --json c4d1759fa3c73b68cd16df8bf8562c4f86268dc6
|
…s four members (objectstack-ai#18474) Part of objectstack-ai#17483. Two authored pages stated the label-resolving dimension class by naming two of its four members. PR objectstack-ai#17470 (card objectstack-ai#16390) deleted `service-analytics`'s private two-member `LOOKUP_TYPES` subset and routed every site that classified a dimension through spec's `referenceTargetOf` instead, so a `user` or `tree` dimension now yields a label exactly as a `lookup` one already did. Both sentences were true when the card was filed and are false on `main` today; the card's `Blocked-by:` fence is spent (objectstack-ai#16390 is closed, PR objectstack-ai#17470 merged as `113050e2c2`). ## The two sentences, before and after All tree readings below were taken in this worktree at `8fe5cb8e5` (the merge base of this branch) between 14:13Z and 14:39Z on 2026-09-16; the diff itself is commit `39528063a`. `origin/main` had moved on to `0fe38a92f` (committed 14:08:52Z) by the last fetch at 14:39Z. ### Site 1 — `content/docs/data-modeling/analytics.mdx`, line 211 (before), line 211 (after) Before: > - **Dimensions** — a `select` dimension returns its option **label** (not the stored > value), a `lookup` / `master_detail` dimension returns the related record's **display > name** (not the FK id), and a `date` dimension with a `dateGranularity` returns a human > bucket label … After: > - **Dimensions** — a `select` dimension returns its option **label** (not the stored > value), a **reference** dimension (any field whose stored value is another record's id, > a `user` person axis included) returns the related record's **display name** (not the FK > id), and a `date` dimension with a `dateGranularity` returns a human bucket label … ### Site 2 — `content/docs/ui/dashboards.mdx`, line 146 (before), lines 146–152 (after) Before: > - A `sortBy` naming a **select** or **lookup** dimension orders by the **display label** > the rows render (the option label / the related record's name), not the stored value or > foreign-key id — and the label is resolved before `limit` applies, so a top-N by name > truncates the right N. Sorting by a measure (the common case) involves no label lookup > and is unaffected. After: > - A `sortBy` naming a **select** or **reference** dimension orders by the **display label** > the rows render (the option label / the related record's name), not the stored value or > foreign-key id — and the label is resolved before `limit` applies, so a top-N by name > truncates the right N. **Reference** is the whole class of fields whose stored value is > another record's id, so a `user` person axis sorts by name too. Sorting by a measure (the > common case) involves no label lookup and is unaffected. ## Why the class is named rather than the members listed Triage's sharpest instruction was: add the missing two, but do not restate the class by listing members if a name for it exists. A name does exist, in three places at once: - spec declares the set as `REFERENCE_VALUE_TYPES` (`packages/spec/src/data/field-value.zod.ts:153`); - the implementation's own header calls it "**the reference class**" (`packages/services/service-analytics/src/dimension-labels.ts:11`), and `createOrderLabelResolver`'s doc comment says a dimension is label-bearing when its field "carries select `options` or belongs to the reference class and names a target"; - `content/docs/**` already uses the same noun in prose — "a reference field" (`content/docs/api/data-api.mdx:199`), "the reference field types" (`content/docs/data-modeling/queries.mdx:327`, `content/docs/protocol/objectql/query-syntax.mdx:739`). So both sentences now name the class. Appending `user` and `tree` to each list would have produced a four-item prose copy of a four-item `const` — a second copy that goes stale the next time the set moves, which is exactly the failure this card is. A named class with a definition ("any field whose stored value is another record's id") stays true when a fifth member lands. Two deliberate choices inside that: - **"reference" is bolded, not backticked.** There is no `reference` *field type*; there is a `reference` *property* on a reference field. Code-formatting the word would have invented a fifth field type. Bold marks it as the class noun, matching how `dashboards.mdx` already bolds **select** and **derived measure**. - **`user` is named as an example, not as a list member.** The card's finding is directional: a reader asking "does a person axis render a name?" read the old enumeration and concluded *no*. A class name alone would leave that reader to classify `user` themselves, which is the very belief gap objectstack-ai#16390 was filed to close. An open example ("a `user` person axis included") answers the question inside the paragraph and — unlike a closed enumeration — claims no completeness, so a future fifth member does not falsify it. ## How `REFERENCE_VALUE_TYPES`' members were confirmed By **module import** of the built package, not by grepping the declaration — a grep over that file also matches four TSDoc comments that quote the same members, and a comment miscounted as a row is a known way to get this wrong by one. Run from `/home/user/objectstack-issue-17483/packages/spec` after `pnpm --filter @objectstack/spec build`: ``` import { REFERENCE_VALUE_TYPES, referenceTargetOf } from '@objectstack/spec/data'; ``` ``` MEMBERS=["lookup","master_detail","user","tree"] SIZE=4 has(user)=true has(tree)=true CONTROL has(select)=false has(text)=false referenceTargetOf({type:user})=sys_user referenceTargetOf({type:tree,reference:"dept"})=dept CONTROL referenceTargetOf({type:select})=undefined ``` `SIZE=4` is read from the live `Set`, so it counts members and cannot count a comment. The two negative controls show the probe discriminates rather than answering `true` to everything. ## Site 2's absence from drift output is structural `content/docs/ui/dashboards.mdx` names neither `master_detail` nor any symbol in PR objectstack-ai#17470's diff — it states the rule by its **inputs** (`sortBy`, `select`, `lookup`, "display label"), and shares no identifier with the **emitter** that implements it. An emitter-only diff therefore cannot list it, on that run or on any run; the drift bot's own "What this run could not see" section states that limit by name. This page was found by a hand sweep over output phrasings, not by the tool. **Its absence from any drift output is structural and is never evidence that it is fine.** Both pages were re-derived by hand for this PR: neither contained the string `user` or `tree` as a field type anywhere (0 hits in each) before this change. ## Changeset: none, and the `files[]` readings behind that Measured, not assumed — the criterion is whether anything **published** moves. | reading (all taken 14:35Z, tree `39528063a`) | value | |---|---| | paths this PR touches | `content/docs/data-modeling/analytics.mdx`, `content/docs/ui/dashboards.mdx` | | package directories containing a touched path | **0** — `content/docs/**` is at the repo root, inside no package | | non-private packages under `packages/**` and `apps/**` | **70** | | of those, packages declaring no `files[]` at all | **0** — every one declares it | | `files[]` entries that escape their own package directory (begin with `..` or `/`) | **0** | | `apps/docs` (`@objectstack/docs`, the renderer of `content/docs/**`) | `private: true`, `files` absent ⇒ never published | | root `package.json` (`@objectstack/spec-monorepo`) | `private: true`, `files` absent | `files[]` globs resolve relative to their own package directory, and no package declares an escaping entry, so no published tarball can contain a repo-root `content/docs/**` path. The one package that consumes these files is private. ⇒ nothing published moves; `skip-changeset` applies. This was measured rather than taken from the docs-only reflex, which has been wrong before on a package whose `files[]` ships its own `CHANGELOG.md` — that shape needs the touched path to sit *inside* a package directory, and neither of these does. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` over the two touched paths, from this worktree. **39 families derived, 39 run, 0 NOT MEASURED, 0 unrun** — reconciled by handing the exit codes back to the same tool (`--ran`, exit 0, 14:38Z). - **38 exit 0.** - Four of them first exited **3 (PREREQUISITE NOT MET)** in an unbuilt fresh worktree (`check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`). Exit 3 is neither a pass nor a finding; after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/client-react` (verify-lock VERDICT command-exit 0, 418 s) all four re-ran at **exit 0**. - **One exit 1, not caused by this diff:** `check:cross-package-test-inputs` reports `packages/cli/test/init-created-files-summary.e2e.test.ts` descending `packages/spec/dist/` with no declared glob reaching inside it. That gate is a scan of test sources and `turbo.json`; this diff contains **0** files with a `.ts/.mts/.mjs/.js/.json` extension, so its inputs are byte-identical to the merge base. Re-run after the builds above: same finding, same exit 1. Repo-wide `pnpm lint` (`eslint . --no-inline-config`) was **narrowed, and the narrowing is measured**, not assumed: 1. **Population, read from eslint's own config:** every `files` entry in `eslint.config.mjs` is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` or narrower. `.mdx` appears in none of them. 2. **File count, read from `--format json`:** running eslint directly on both touched paths returns, for each, `File ignored because no matching configuration was supplied.` — `errorCount: 0`, `warningCount: 1`, **zero rules applied**. This diff contributes 0 files to the linted population. 3. **Invariance for untouched files:** type-aware linting is not enabled anywhere in this config — no `parserOptions.project`, no typed `@typescript-eslint` rules (stated at `eslint.config.mjs:328`) — so no `.mdx` edit can move the verdict on any file it does not contain. Also run as an MDX parse check: `pnpm exec fumadocs-mdx` in `apps/docs`, **exit 0** — both pages still compile. CI's two path-scheduled jobs (`Test Core` shards and `Build Docs`) have no local invocation and are declared to CI. ## Risk Prose only, in two bullets, on two pages. No `packages/**` file is touched; the spec fact the prose now names was read out of the built package, not authored here. Rollback is a revert of one commit. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16390
Clause-②: no — no symbol is published, no error code is minted, and no authorable key is added.
DatasetDimensionSchema(and all ofpackages/spec) is read, never written. What changes is a RETURNED VALUE: auserdimension's cell was the stored user id and is now the referenced user's display name. Re-tested against the actual diff after writing it:git diffoverpackages/services/service-analytics/srcadds no line containingexport.What this changes
packages/specdeclares one reference class —REFERENCE_VALUE_TYPES=lookup,master_detail,user,tree, "value points at another record … a record-id string in stored form" — andservice-analyticsalready treats it as one class where it annotates measure result types (measure-result-type.tsimports that very set). The label resolver, one file away, hand-wrote a two-member subset of it, so a single dataset query answered one axis with a name and the other with a raw id for two fields that differ in one word:packages/services/service-analytics/src/dimension-labels.tsnow asks spec'sreferenceTargetOf— the declared SINGLE arbiter of "what does this reference field point at" — at all three sites that classified a dimension: the display pass inresolveDimensionLabels, plus both classification points of the#3680order-label hook (isLabelBearing, and the label fetch beside it). The privateLOOKUP_TYPESset is gone rather than extended by two literals, because a hand-written subset of a declared class is the defect, not the two members it happened to be missing.Two consequences that a
LOOKUP_TYPES.add('user')patch would not have had:userfield authored withoutreferenceresolves too.sys_useris a constant of the type, whichreferenceTargetOfmaterializes. That shape reaches production —packages/objectql/src/query-expression-conformance.test.tscaptures one — and the oldmeta.reference &&test dropped it silently.#3602read scope reaches the new members. Turning a user id into a name IS a read ofsys_user. It travels the sameLabelScopeResolverpath every other member travels: the referenced object's own RLS is resolved and ANDed into the id lookup, and an unresolvable scope still fails closed to the raw id rather than fetching unscoped. This had to land WITH the widening, not after it — a resolution that readsys_userunscoped would leak "who is who" to a caller who cannot readsys_user.Route: (a), as ruled.
packages/specis untouchedRoute (b) — a
displayFieldkey on the strictDatasetDimensionSchema— is not taken and was not needed: nothing about (a) required a spec type to widen, so the prohibition never fired. Nor is a name denormalised into the result table.DatasetDimensionSchema.typeis['string','number','date','boolean','lookup']— it has nousermember at all — which is the structural reason the fix belongs at the resolution site: there is no author-facing declaration to fix it with.Zone-3 re-verification — the card's repro is STILL LIVE on today's
mainThe card was filed 2026-09-06 and #16778 landed in this package today, so the premises were re-measured rather than carried across, at base
9788f1e91:dimension-labels.tsstill holds it, and its last touch is#17061, before this card.Field.user()still returns{ type: 'user', reference: 'sys_user' }andField.lookup(...)still returns{ type: 'lookup', reference: … }— asserted by the new pin itself, on every run, from the real builders.type: 'lookup'still does not change the result: outside thedatebranch the resolver reads only the OBJECT field's type. Pinned both ways (a user axis declaredlookupand one declaredstringresolve identically).main: 7 failed / 5 passed. The defect reproduces.Verification
Base
fd62a66b7, reported at HEAD44918e39d.Tests —
pnpm --filter @objectstack/service-analytics test: 106 files / 2284 tests passed.typecheck: clean, andtsc --noEmit --listFiles | grep -cconfirms the new test file is one of the files that program compiles (1), so the typecheck really covers it.Ablation (C5). The fix is committed first; the mutation restores the pre-fix classification verbatim inside
referenceLabelTarget(the two-member subset plus themeta.referencetest). On-disk proof, not an editor exit code — the deleted text goes 1 to 0 and the injected marker 0 to 1, and the blob hash moves off the HEAD blob. Resolution path: the pin imports the subject relatively inside its own package, so it reads source, notdist(pnpm check:test-source-aliaspasses, and the spec dependency it does read fromdistis unchanged by the mutation).Direction was predicted before running and is written into the test header: ordinary, no inversion, no count movement — red exactly where a
userortreeaxis is load-bearing, green for everylookup,master_detail,select, unresolved-id, no-display-field and fail-closed case.master_detailwas already inside the old subset, so it is a control here, not a casualty. Predicted 7 red in the new file + 3 in the sibling; measured exactly that. The 7 also match the pre-fix run above, so the ablation reproduces the original defect rather than some third state.Restoration proven BY STATE, not by exit code:
Gates. Derived from the actual diff (
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack), all 60 run, exit codes captured before any pipe, reconciled with--ran: 60 derived, 57 run green, 3 NOT MEASURED, 0 unrun. The three arecheck:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt— each exits 3, PREREQUISITE NOT MET, because each reads built output for the whole workspace. Those are CI's repo-scale build, not a per-card local run; they are recorded as NOT MEASURED, not as passes. One caveat on the derivation itself: it was taken at44918e39d, by which timeorigin/mainhad moved one derivation-source script (scripts/measure-test-shard-timings.mjs); the family it feeds (report-test-timings.mjs --self-test) is in the list above and ran green.Lint — a declared narrowing, with its three pieces of evidence.
pnpm lint(=eslint . --no-inline-configover the repo root) is CI's run. Locally this diff was linted file-by-file at HEAD44918e39d:--format jsonreports 3 files, 0 errors, 0 warnings (the changeset markdown is not a lint target — eslint answers "File ignored because no matching configuration was supplied" for it, so 3 is the diff's whole lint surface). The narrowing excludes nothing, because this repo runs oneeslint.config.mjswhich never enables type-aware linting for ANY file — noparserOptions.project, no typed@typescript-eslintrules, stated and positively controlled in the config's own header aboveQUERY_OPTIONS_TEST_GLOBS. With no cross-file type program, an edit confined to three files cannot move the verdict on a file it did not touch.Acceptance notes
C3 — the symmetry pin.
packages/services/service-analytics/src/__tests__/dataset-reference-dimension-labels.test.tsruns ONEAnalyticsService.queryDatasetover one dataset holding aField.lookup()axis and aField.user()axis with anavgmeasure, and asserts a display name in BOTH cells of the same row.queryDatasetis exactly whatPOST /api/v1/analytics/dataset/querydispatches to (rest-server.tscallssvc.queryDatasetand does nothing to the rows); the route itself lives inpackages/rest, outside this card's declared file surface, so it is not re-tested here.C4 — the negative control, stated. An unresolvable user degrades to the raw id and the query still answers. Three shapes are pinned: an orphaned or RLS-hidden user id (the row keeps the id; a sibling row in the same result still resolves), a
sys_usercarrying no display field at all (every id stays raw), and asys_userunknown to the engine (same). A fourth, the fail-closed one, is pinned separately: when the read-scope provider throws forsys_user, nothing is fetched and the id renders. Nothing throws, nothing blanks, and no id-labelled chart turns into an error — this is the pre-existing contract for an unresolved lookup id, now reached by the same code path.Triage's stated acceptance condition, and one correction to it. The triage comment asked that
dimension-labels.test.ts:167'sexpect(scopeCalls).toBe(0)be converted into a positive four-member assertion. Read on the tree, that assertion is about a select dimension, which resolves from field metadata and reads no other object — it stays correct after the widening and was kept, with its now-stale trailing comment rewritten. The positive counterpart it asked for was ADDED beside it instead:#16390 — a user and a tree dimension resolve the REFERENCED object scope, same as lookupasserts the resolver is consulted forcrm_account,sys_userandtaskin order, and that each scope reachesfetchRecordLabels.Out of scope, noted, not filed. Two further two-member spellings of the same class survive in this package, both outside this card's declared file surface:
plugin.ts—relationshipResolveradmits onlylookup/master_detailwhen mapping a datasetincludeto a joined table. Reachable throughDataset.include, not through the dimension path this card fixes, and auserjoin has a fixed target so it is a different question than the label read. Carrier: the next PR that touches dataset joins inservice-analytics.dataset-executor.ts(order-hook prose) andanalytics-service.ts(a doc comment) describe the sort behaviour asselectorlookup/master_detail. The BEHAVIOUR is already corrected by this PR — both readcreateOrderLabelResolver, which now covers the class, so a person axis sorts by name from this commit on. Only the two comments are now understated. Carrier: the same next PR; comment-only drift, filed nowhere.Neither is a filed card: both are stale prose or an untested adjacent path, i.e. observations, not reproducible defects, contract breaches, or metadata traps.
Docs drift check — answered
Re-derived on the tree the bot actually read, not a worktree cut from an older
main: merge commit46332726694e99851d1e794aed90ee80f4ab3cf1(head44918e39dainto basec4d1759fa3), fetched and checked out per the comment's own recipe, thennode scripts/docs-audit/affected-docs.mjs --json c4d1759fa3c73b68cd16df8bf8562c4f86268dc6. Same numbers as the advisory: 27 docs (21 hand-written + 6 release-owned), 7 anchors, 9 package-mention fallback rows.Why all 21 rows arrived, in one sentence. Every one of the 27 rows is anchored on the same single item —
master_detail (literal, a string literal in LOOKUP_TYPES). This diff DELETES theLOOKUP_TYPESset, whose members were the literals'lookup'and'master_detail', so every page that spells the field typemaster_detailanywhere was selected. That is the anchor doing its job precisely; it is not a wrong row. But it also means the listing carries no signal about which page states the RULE this diff changed, so each page was read against the change rather than dismissed as a class.Per-page verdict — the 21 hand-written pages. One is falsified. The other 20 are not, and they divide into three groups by what their
master_detailoccurrence actually says:content/docs/data-modeling/analytics.mdx:209-213. The falsifying sentence: "aselectdimension returns its option label (not the stored value), alookup/master_detaildimension returns the related record's display name (not the FK id), and adatedimension … returns a human bucket label". This is the page that restates exactly the rule this PR widened. After this change auserand atreedimension also return the related record's display name, so the two-member enumeration is now wrong by omission — and wrong in the direction that matters, because a reader asking "does a person axis render a name?" reads this list and concludes it does not. Reported below, ⛔ not edited.data-modeling/field-types.mdx,field-type-decision-tree.mdx,fields.mdx,relationships.mdx,index.mdx,validation-rules.mdx,getting-started/common-patterns.mdx,quick-reference.mdx,deployment/cli.mdx,deployment/troubleshooting.mdx,protocol/objectql/types.mdx. Each namesmaster_detailas a field type — cascade delete, parent-child modelling,Field.masterDetail(), delete behaviour. None makes a claim about analytics, dimensions or what a grouped value renders as. This diff changes no field type and no delete behaviour.api/data-api.mdx:24,automation/hook-bodies.mdx:103,data-modeling/queries.mdx:328,protocol/objectql/query-syntax.mdx:739and:803,protocol/objectui/widget-contract.mdx:162. These describeexpand/ the relational field family and already enumeratelookup/master_detail/user/tree(two of them namingREFERENCE_VALUE_TYPESoutright). They agree with this change rather than being falsified by it — they document the same class this diff stops splitting.api/error-catalog.mdx,protocol/kernel/error-handling.mdx,permissions/authorization.mdx,protocol/objectql/schema.mdx. The first three describe the master-access gate and required-field validation for amaster_detail; this diff mints no error code, changes no status and touches no admission path. On the fourth,schema.mdx:298(| reference | string | lookup, master_detail | Target object for relationships. |) understates which types carryreference— but it understated it identically before this diff, sincereferenceTargetOfandREFERENCE_VALUE_TYPESpredate it. Pre-existing, not caused here, and reported as an observation rather than a finding.⭐ The half the bot states it can never do — hand-checked emitter-blind set. The advisory says a page that states a rule by its INPUTS shares no identifier with the emitter, so an emitter-only diff cannot list it on this or any run. So the analytics / dataset / dimension / chart / report / dashboard surface was swept by hand for pages that describe what a dimension axis renders WITHOUT naming
referenceTargetOf,REFERENCE_VALUE_TYPES,dimension-labels,createOrderLabelResolverormaster_detail. Two sweeps over all ofcontent/docsoutsidereleases/: one on output phrasings (display name,display label,FK id,raw id,option label,stored value,presentation-ready,resolved server-side), one on rendering verbs neardimension/axis(shows the,renders,returns the,reads the,id instead,name instead). It found a second falsified page the bot did not and could not list:content/docs/ui/dashboards.mdx:146-150. The falsifying sentence: "AsortBynaming a select or lookup dimension orders by the display label the rows render (the option label / the related record's name), not the stored value or foreign-key id". This states the#3680sort-key rule, which this diff widened throughcreateOrderLabelResolver.isLabelBearing: asortBynaming auserortreedimension now orders by the display label too. The page names none of this change's identifiers — notmaster_detail, notOrderLabelResolver— which is exactly why no anchor could reach it. Reported below, ⛔ not edited. (Its omission ofmaster_detailis separately pre-existing and not caused here.)Also read and cleared in that sweep, so the next reader need not redo them:
references/ui/dataset.mdx:57andreferences/data/analytics.mdx:129(generated reference tables mirroringDatasetDimensionSchema.type/ the Cube dimension shape —packages/specis untouched, so neither moved);references/ui/report.mdx,references/ui/chart.mdx,references/api/analytics.mdx,references/api/sortability.mdx,capabilities/analytics.mdx(name dimensions but state nothing about the rendered value); andui/reports.mdx:157-159, which explicitly delegates the ordering rule todata-modeling/analytics.mdxand says it "does not repeat" it — so it carries no independent claim to falsify.The 6 release-owned pages: read, and left alone.
releases/implementation-status.mdx,releases/v12.mdx,releases/v17/17-0.mdx,releases/v17/17-3.mdx,releases/v17/17-4.mdx,releases/v9.mdx. ⛔ Nothing undercontent/docs/releases/**was edited. Two of them do state this rule —v9.mdx:114-115("aselectdimension shows its option label … alookup/master_detaildimension shows the …") and17-0.mdx:1385("selections sort by display label for select/lookup dimensions") — and neither is falsified, for a reason rather than by exemption: a release page is a dated record of what a given release shipped, and both sentences remain true of v9 and 17.0 respectively. A later widening does not retro-falsify the note that describes the narrower behaviour at its own release. If that reading is wrong, it is a report, not an edit.Two findings handed to the
domain:servicesseat, ⛔ not edited here.content/docs/**isdomain:devx, so neither page is touched in this PR:content/docs/data-modeling/analytics.mdx:211-212— "alookup/master_detaildimension returns the related record's display name (not the FK id)" should name all four members of the reference class.content/docs/ui/dashboards.mdx:146-147— "AsortBynaming a select or lookup dimension orders by the display label" should name the reference class, notlookupalone.Neither is a generated block, so the generated-surface exception does not apply.
Generated by Claude Code
Generated by Claude Code